﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using Microsoft.VisualStudio.TestPlatform.UnitTestFramework;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MugenInjection;
using MugenInjection.Attributes;
using MugenInjection.Bindings;
using MugenInjection.Delegates;
using MugenInjection.Exceptions;
using MugenInjection.Infrastructure;
using MugenInjection.Interface;
using MugenInjection.Interface.Behaviors;
using MugenInjection.Interface.Components;
using MugenInjectionTest.Converters;
using Should;
using TestModels.FakeObject;

namespace MugenInjectionTest.Core
{
    public partial class InjectorTest
    {
        [TestMethod]
        public void ConflictBindingsTest()
        {
            using (MugenInjector injector = GetInjector())
            {
                injector.Bind<DuplicateClass>().ToSelf();
                injector.Bind<DuplicateClass>().ToConstant(new DuplicateClass());
                TestUtils.Catch<ConflictBindingException>(() => injector.Get<DuplicateClass>());
            }
        }

        [TestMethod]
        public void NullBindingResultTest()
        {
            using (MugenInjector injector = GetInjector())
            {
                injector.Settings.ThrowErrorForNullableBinding = true;
                injector.Bind<DuplicateClass>().ToMethod<DuplicateClass>(context => null);

                TestUtils.Catch<NullableBindingException>(() => injector.Get<DuplicateClass>());
                injector.Settings.ThrowErrorForNullableBinding = false;
                var duplicateClass = injector.Get<DuplicateClass>();
                duplicateClass.ShouldBeNull();
            }
        }

        [TestMethod]
        public void CycleBindingsTest()
        {
            using (MugenInjector injector = GetInjector())
            {
                injector.Bind<CycleClass>().ToSelf();
                TestUtils.Catch<CyclicDependencyException>(() => injector.Get<CycleClass>());
            }
        }

        [TestMethod]
        public void CycleBindingsWithCustomBehaviorTest()
        {
            using (MugenInjector injector = GetInjector())
            {
                var behaviorManagerComponent = injector.Components.Get<IBehaviorManagerComponent>();
                injector.Bind<CycleClass>().ToSelf();
                TestUtils.Catch<CyclicDependencyException>(() => injector.Get<CycleClass>());

                var cycleClass = new CycleClass(null);

                //Configure custom behavior.
                behaviorManagerComponent.Add<ICycleDependencyBehavior>(new CustomCycleBehaviorResolveUnregisteredTypeBehavior() { Func = () => cycleClass });
                var @class = injector.Get<CycleClass>();
                @class.Class.ShouldEqual(cycleClass);

                //Remove behavior
                behaviorManagerComponent.Remove<ICycleDependencyBehavior>();
                TestUtils.Catch<CyclicDependencyException>(() => injector.Get<CycleClass>());
            }
        }

        [TestMethod]
        public void ResolveUnregisteredTypeBehaviorTest()
        {
            using (MugenInjector injector = GetInjector())
            {
                var behaviorManagerComponent = injector.Components.Get<IBehaviorManagerComponent>();
                TestUtils.Catch<BindingNotFoundException>(() => injector.Get<string>());

                const string method = "Method";

                behaviorManagerComponent.Add<IResolveUnregisteredTypeBehavior>(new CustomCycleBehaviorResolveUnregisteredTypeBehavior() { Func = () => method });
                var st = injector.Get<string>();
                st.ShouldEqual(method);

                //Remove behavior
                behaviorManagerComponent.Remove<IResolveUnregisteredTypeBehavior>();
                TestUtils.Catch<BindingNotFoundException>(() => injector.Get<string>());
            }
        }

        [TestMethod]
        public void TestParamsResolve()
        {
            using (MugenInjector injector = GetInjector())
            {
                //Test empty params
                var paramClass = injector.Get<ParamsClass>();
                paramClass.Objs.ShouldBeEmpty();

                //Test bind one value
                injector.Bind<IDisposable>().ToConstant(injector);
                paramClass = injector.Get<ParamsClass>();
                paramClass.Objs.Length.ShouldEqual(1);
                paramClass.Objs.Contains(injector).ShouldBeTrue();

                //Test bind two value
                injector.Bind<IDisposable>().ToConstant(injector);
                paramClass = injector.Get<ParamsClass>();
                paramClass.Objs.Length.ShouldEqual(2);
                paramClass.Objs[0].ShouldEqual(injector);
                paramClass.Objs[1].ShouldEqual(injector);

                //Test array
                var array = new IDisposable[] { new ReleaseDisposed() };
                injector.Bind<IDisposable[]>().ToConstant(array);
                paramClass = injector.Get<ParamsClass>();
                paramClass.Objs.Length.ShouldEqual(1);
                paramClass.Objs[0].ShouldEqual(array[0]);
            }

            //Test named params
            using (MugenInjector injector = GetInjector())
            {
                injector.Bind<IDisposable>().ToConstant(injector);
                var paramClass = injector.Get<ParamsClassNamed>();
                paramClass.Objs.ShouldBeEmpty();

                injector.Bind<IDisposable>().ToConstant(injector).NamedBinding("test");
                injector.Bind<IDisposable>().ToConstant(injector).NamedBinding("test");
                paramClass = injector.Get<ParamsClassNamed>();
                paramClass.Objs.Length.ShouldEqual(2);
                paramClass.Objs[0].ShouldEqual(injector);
                paramClass.Objs[1].ShouldEqual(injector);
            }

            //Test named array
            using (MugenInjector injector = GetInjector())
            {
                var array = new IDisposable[] { new ReleaseDisposed() };
                injector.Bind<IDisposable>().ToConstant(new ReleaseDisposed());
                injector.Bind<IDisposable[]>().ToConstant(array).NamedBinding("test");
                injector.Bind<IDisposable>().ToConstant(injector).NamedBinding("test");
                var paramClass = injector.Get<ParamsClassNamed>();
                paramClass.Objs.Length.ShouldEqual(1);
                paramClass.Objs[0].ShouldEqual(array[0]);
            }

            //Test optional params array
            using (MugenInjector injector = GetInjector())
            {
                injector.Bind<IDisposable>().ToConstant(injector);
                var paramClass = injector.Get<ParamsClassOptional>();
                paramClass.Objs.Length.ShouldEqual(1);
                paramClass.Objs.Contains(injector).ShouldBeTrue();
            }

            //Test optional params
            using (MugenInjector injector = GetInjector())
            {
                var paramClass = injector.Get<ParamsClassOptional>();
                paramClass.Objs.ShouldBeEmpty();
            }
        }

        [TestMethod]
        public void ThreadSafetyResolveTest()
        {
            const int threadCount = 400;
            const string st1 = "st1";
            var enumerableCollection = new List<EnumerableCollectionClass>();
            var duplicateClasses = new List<DuplicateClass>();
            var list = new List<ParamsClass>();
            using (var injector = GetInjector())
            {
                injector.BindWithManualBuild<string>().ToConstant(st1).Build();
                injector.BindWithManualBuild<IDisposable>().ToConstant(injector).Build();

                ThreadInvoker.Invoke(() => enumerableCollection.Add(injector.Get<EnumerableCollectionClass>()), threadCount);
                ThreadInvoker.Invoke(() => duplicateClasses.Add(injector.Get<DuplicateClass>()), threadCount);
                ThreadInvoker.Invoke(() => list.Add(injector.Get<ParamsClass>()), threadCount);

                enumerableCollection.Count.ShouldEqual(threadCount);
                duplicateClasses.Count.ShouldEqual(threadCount);
                list.Count.ShouldEqual(threadCount);
            }
        }

        [TestMethod]
        public void ComplexTypeTest1()
        {
            const string st = "st1";
            using (var injector = GetInjector())
            {
                injector.Bind<string>().ToConstant(st);
                var enumerable = injector.Get<IEnumerable<System.Func<string>>>();
                enumerable.Count().ShouldEqual(1);
                var func = enumerable.First();
                func().ShouldEqual(st);
            }
        }


        [TestMethod]
        public void ComplexTypeTest2()
        {
            const string st1 = "st1";
            const string st2 = "st1";
            const string st3 = "st1";
            using (var injector = GetInjector())
            {
                injector.Bind<string>().ToConstant(st1);
                injector.Bind<string>().ToConstant(st2);
                injector.Bind<string>().ToConstant(st3);
                var enumerable = injector.Get<IEnumerable<System.Func<Lazy<Managed<IEnumerable<WithSettings<string>>>>>>>();
                enumerable.Count().ShouldEqual(1);
                var value = enumerable.First()().Value.Value;
                value.Count().ShouldEqual(3);
                value.Any(settings => settings.Value == st1).ShouldBeTrue();
                value.Any(settings => settings.Value == st2).ShouldBeTrue();
                value.Any(settings => settings.Value == st3).ShouldBeTrue();
            }
        }

        [TestMethod]
        public void IfBindingBuilderIsConfigureBindingNotBuildTest()
        {
            using (var injector = GetInjector())
            {
                var builder = new ConfiguringBindingBuilder { IsConfiguring = true };
                injector.Bindings.AddBindingBuilder(builder);
                injector.Bindings.ShouldBeEmpty();
                builder.IsBuilded.ShouldBeFalse();
                builder.IsConfiguring = false;
                injector.Bindings.Count.ShouldEqual(1);
                builder.IsBuilded.ShouldBeTrue();
            }
        }

        [TestMethod]
        public void InitializableTypeBindingTest()
        {
            using (var injector = GetInjector())
            {
                injector.Bind<Initializable>().ToSelf();
                var initializable = injector.Get<Initializable>();
                initializable.IsInitialized.ShouldBeTrue();
            }

            using (var injector = GetInjector())
            {
                var initializable = injector.Get<Initializable>();
                initializable.IsInitialized.ShouldBeTrue();
            }

            using (var injector = GetInjector())
            {
                injector.Bind<Initializable>().ToSelf().InSingletonScope();
                var initializable = injector.Get<Initializable>();
                initializable.IsInitialized.ShouldBeTrue();
                initializable.IsInitialized = false;
                initializable = injector.Get<Initializable>();
                initializable.IsInitialized.ShouldBeFalse();
            }
        }

        [TestMethod]
        public void InitializableMethodBindingTest()
        {
            using (var injector = GetInjector())
            {
                injector.Bind<Initializable>().ToMethod(context => new Initializable());
                var initializable = injector.Get<Initializable>();
                initializable.IsInitialized.ShouldBeTrue();
            }

            using (var injector = GetInjector())
            {
                injector.Bind<Initializable>().ToMethod(context => new Initializable()).InSingletonScope();
                var initializable = injector.Get<Initializable>();
                initializable.IsInitialized.ShouldBeTrue();
                initializable.IsInitialized = false;
                initializable = injector.Get<Initializable>();
                initializable.IsInitialized.ShouldBeFalse();
            }
        }

        [TestMethod]
        public void StartableTypeBindingTest()
        {
            Startable startable = null;
            using (var injector = GetInjector())
            {
                injector.Bind<Startable>().ToSelf();
                startable = injector.Get<Startable>();
                startable.IsStarted.ShouldBeTrue();
            }
            startable.IsStoped.ShouldBeFalse();

            using (var injector = GetInjector())
            {
                startable = injector.Get<Startable>();
                startable.IsStarted.ShouldBeTrue();
            }
            startable.IsStoped.ShouldBeFalse();

            using (var injector = GetInjector())
            {
                injector.Bind<Startable>().ToSelf().InSingletonScope();
                startable = injector.Get<Startable>();
                startable.IsStarted.ShouldBeTrue();
                startable.IsStarted = false;
                startable = injector.Get<Startable>();
                startable.IsStarted.ShouldBeFalse();
            }
            startable.IsStoped.ShouldBeTrue();
        }

        [TestMethod]
        public void StartableMethodBindingTest()
        {
            Startable startable = null;
            using (var injector = GetInjector())
            {
                injector.Bind<Startable>().ToMethod(context => new Startable());
                startable = injector.Get<Startable>();
                startable.IsStarted.ShouldBeTrue();
            }
            startable.IsStoped.ShouldBeFalse();

            using (var injector = GetInjector())
            {
                injector.Bind<Startable>().ToMethod(context => new Startable()).InSingletonScope();
                startable = injector.Get<Startable>();
                startable.IsStarted.ShouldBeTrue();
                startable.IsStarted = false;
                startable = injector.Get<Startable>();
                startable.IsStarted.ShouldBeFalse();
            }
            startable.IsStoped.ShouldBeTrue();
        }

        [TestMethod]
        public void ConcurrentBindingsTest()
        {
            using (var injector = GetInjector())
            {
                injector.Bind<string>().ToConstant("test");
                var listTypes = new Dictionary<Type, Type>();
                listTypes.Add(typeof(IList<string>), typeof(List<string>));
                listTypes.Add(typeof(IInitializable), typeof(Initializable));
                listTypes.Add(typeof(IStartable), typeof(Startable));
                listTypes.Add(typeof(FakeActivator), typeof(FakeActivator));
                listTypes.Add(typeof(IBindingBuilder), typeof(ConfiguringBindingBuilder));
                listTypes.Add(typeof(ParamsClassOptional), typeof(ParamsClassOptional));

                foreach (var listType in listTypes)
                {
                    injector.Bind(listType.Key).To(listType.Value);
                }
                const int threadCount = 1000;
                var eventWaitHandle = ThreadInvoker
                    .InvokeAsync(() =>
                                     {
                                         ThreadInvoker.Invoke(() =>
                                                                  {
                                                                      foreach (var listType in listTypes)
                                                                      {
                                                                          var s = Guid.NewGuid().ToString();
                                                                          injector.BindWithManualBuild(listType.Key)
                                                                                  .To(listType.Value)
                                                                                  .NamedBinding(s)
                                                                                  .Build();
                                                                      }
                                                                  }, threadCount);
                                     });
                ThreadInvoker.Invoke(() =>
                                         {
                                             foreach (var listType in listTypes)
                                             {
                                                 listType.Value.IsInstanceOfType(injector.Get(listType.Key)).ShouldBeTrue();
                                                 if (listType.Key == typeof(IList<string>))
                                                     listType.Value.IsInstanceOfType(injector.Get<System.Func<IList<string>>>()()).ShouldBeTrue();
                                             }
                                         }, threadCount);
                eventWaitHandle.WaitOne();
            }
        }

        public class Initializable : IInitializable
        {
            #region Properties

            public bool IsInitialized { get; set; }

            #endregion

            #region Implementation of IInitializable

            /// <summary>
            ///     Initializes the instance. Called during activation.
            /// </summary>
            public void Initialize()
            {
                IsInitialized = true;
            }

            #endregion
        }

        public class Startable : IStartable
        {
            #region Properties

            #endregion

            #region Implementation of IStartable
            public bool IsStarted { get; set; }

            public bool IsStoped { get; set; }


            /// <summary>
            ///     Starts this instance. Called during activation.
            /// </summary>
            public void Start()
            {
                IsStarted = true;
            }

            /// <summary>
            ///     Stops this instance. Called during deactivation.
            /// </summary>
            public void Stop()
            {
                IsStoped = true;
            }

            #endregion
        }

        public class ConfiguringBindingBuilder : IBindingBuilder
        {
            #region Properties

            public bool IsBuilded { get; set; }

            #endregion

            #region Implementation of IBindingBuilder

            /// <summary>
            ///     Gets the <see cref="IInjector" /> that owns the <see cref="IBindingBuilder" />.
            /// </summary>
            public IInjector Injector { get; private set; }

            /// <summary>
            ///     Gets the specified service types.
            /// </summary>
            public IList<Type> Services { get; private set; }

            /// <summary>
            ///     Gets the settings for the current <see cref="IBindingBuilder" />.
            /// </summary>
            public ISettings Settings { get; private set; }

            /// <summary>
            ///     Indicates that the binding builder is configuring.
            /// </summary>
            public bool IsConfiguring { get; set; }

            public event BindingBuildingDelegate Building;

            public event BindingBuildedDelegate Builded;

            /// <summary>
            ///     Builds the current <see cref="IBindingBuilder" />.
            /// </summary>
            /// <returns>
            ///     The builded <see cref="IBinding" />.
            /// </returns>
            public IBinding Build()
            {
                IsBuilded = true;
                return new ConstantBinding(new object(), new List<Type>() { typeof(object) },
                                           new List<IInjectionParameter>(), context => true);
            }

            #endregion
        }

        public class CustomCycleBehaviorResolveUnregisteredTypeBehavior : ICycleDependencyBehavior, IResolveUnregisteredTypeBehavior
        {
            #region Property

            [Inject]
            public System.Func<object> Func { get; set; }

            #endregion

            #region Implementation of ICycleDependencyBehavior

            /// <summary>
            /// Resolve circular dependencies.
            /// </summary>
            /// <param name="bindingContext">The specified <see cref="IBindingContext"/>.</param>
            /// <param name="result"> </param>
            /// <returns></returns>
            bool ICycleDependencyBehavior.Resolve(IBindingContext bindingContext, out object result)
            {
                result = Func();
                return true;
            }

            #endregion

            #region Implementation of IResolveUnregisteredTypeBehavior

            /// <summary>
            /// Resolve unregistered type.
            /// </summary>
            /// <param name="bindingContext">The specified <see cref="IBindingContext"/>.</param>
            /// <param name="result"> </param>
            /// <returns></returns>
            bool IResolveUnregisteredTypeBehavior.Resolve(IBindingContext bindingContext, out object result)
            {
                result = Func();
                return true;
            }

            #endregion
        }

        public class DuplicateClass
        {
        }

        public class CycleClass
        {
            public CycleClass Class { get; set; }

            public CycleClass(CycleClass cycleClass)
            {
                Class = cycleClass;
            }
        }

        public class ParamsClass
        {
            public IDisposable[] Objs { get; private set; }

            public ParamsClass(params IDisposable[] objs)
            {
                Objs = objs;
            }
        }

        public class ParamsClassNamed
        {
            public IDisposable[] Objs { get; private set; }

            public ParamsClassNamed([NamedParameter("test")]params IDisposable[] objs)
            {
                Objs = objs;
            }
        }

        public class ParamsClassOptional
        {
            public IDisposable[] Objs { get; private set; }

            public ParamsClassOptional([OptionalParameter()]params IDisposable[] objs)
            {
                Objs = objs;
            }
        }

        public class ParamsClassOptionalError
        {
            public IDisposable[] Objs { get; private set; }

            public ParamsClassOptionalError([OptionalParameter("error")]params IDisposable[] objs)
            {
                Objs = objs;
            }
        }

        public class EnumerableCollectionClass
        {
            public IEnumerable<string> Strings { get; set; }

            public EnumerableCollectionClass(IEnumerable<string> strings)
            {
                Strings = strings;
            }
        }
    }
}
